EffectiveJava读书笔记(九):并发

您所在的位置:网站首页 scheduledthreadPool 删除任务 EffectiveJava读书笔记(九):并发

EffectiveJava读书笔记(九):并发

2023-04-07 09:36| 来源: 网络整理| 查看: 265

Effective Java 读书笔记九并发同步访问共享的可变数据避免过度同步使用线程池管理线程不要手动 new 线程并发工具优先于 wait 和 notify线程安全性的文档化慎用延迟初始化不要依赖于线程调度器避免使用线程组 ThreadGroup Effective Java 读书笔记(九):并发 同步访问共享的可变数据 当共享可变数据时,不进行同步有两个坏处:一是可能导致数据处于不一致的状态,二是一个线程的修改无法马上对其他线程可见。同步带来两个效果:原子性、可见性。Synchronized 既能保证原子性,也能保证可见性。volatile 则只能保证可见性。增量操作符 ++ 不是原子的。共享不可变的数据是安全的,不需要加锁同步。对于共享对象,不去修改其内容,只同步其引用,这种对象可以称为事实上不可变的。这种对象引用从一个线程传递到其他线程,被称为安全发布(safe publication)。 避免过度同步

为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法,同时要尽量限制同步区域内部的工作量。

使用线程池管理线程,不要手动 new 线程

从 Java5 开始引入了 Executor Framework,这是一个灵活的基于接口的任务执行工具,执行的是 Runnable 或 Callable(后者有返回值)。

在实际做项目时,不推荐使用 Executors 去创建线程池,而是通过 ThreadPoolExecutor 的方式。为什么呢?Executors 返回的线程池有一些缺点: 1. FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2. CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

代码不受约束是很可怕的事情。

从本质上将,Executor Framework 所做的工作是执行,犹如 Collections Framework 所做的工作是聚集一样。

并发工具优先于 wait 和 notify

wait 和 notify 有什么缺点呢? 1. 只有一个资源,但是可能用 notify all 唤醒了所有等待线程。 2. 在没有通知的情况下,等待线程也可能苏醒,被称为虚假唤醒 spurious wakeup。

wait 的标准用法如下:

synchronized (obj) {while (condition does not hold) {obj.wait(); // 释放锁,等待被唤醒}// 执行其他操作}

始终使用 wait 循环模式调用 wait 方法,循环会在等待之前和之后测试条件。

在有现成的并发工具时,不要自己用 wait、notify 去控制并发,比如用 ConcurrentMap,好过自己控制并发。ConcurrentMap 相对于 Collections.synchronizedMap 性能更好。

线程安全性的文档化

当一个类的实例或静态方法被并发使用的时候,这个类的行为如何,是该类与其客户端程序建立的约定的重要组成部分。如果没有在文档中描述类的并发性情况,使用这个类的程序员将不得不做出某些假设。如果假设错误,可能导致缺少同步,或过度同步。

线程安全有多种级别: 1. 不可变的 immutable:String、Long、BigInteger。 2. 无条件的线程安全:无需任何外部同步,比如 Random、ConcurrentHashMap。 3. 有条件的线程安全:部分方法需要外部同步,比如 Collections.synchronizedMap 的迭代器。 4. 非线程安全:比如 ArrayList 和 HashMap。 5. 线程对立的:外部同步也白搭的,不能线程安全地被多个线程并发使用。

在无条件的线程安全中,需要使用私有锁对象,防止外部客户端访问锁,影响对象的同步。私有锁对象特别适合于那些为继承而设计的类,如果使用自身实例作为锁,子类很容易在无意中妨碍基类的操作,反之亦然。出于不同的目的而使用相同的锁,子类和基类可能会互相绊住对方的脚。

有条件的线程安全,需要在文档中说明:在执行某些方法调用序列时,它们的客户端程序必须获得哪把锁。

慎用延迟初始化

是否要用延迟初始化,要看具体情况。延迟初始化降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销(会影响初次访问的性能)。若非必要,就不要使用延迟初始化。

一切都是权衡,很多服务器系统会在起到后,提前初始化一些大对象后,或者进行一定程度的预热后,再对外提供服务。

如果出于性能的考虑要对静态域使用延迟初始化,推荐使用 lazy initialization holder class 模式:

private static class FieldHolder {static final FieldType field = computeFieldValue();}static FieldType getInstance() {return FieldHolder.field;}

如果出于性能考虑,要对实例域进行初始化,就使用双重检查模式 double check idiom:锁 + volatile。

private volatile FieldType field;FieldType getInstance() {FieldType result = field;if (result == null) {synchronized (this) {result = field;if (result == null) {field = result = computeFieldValue();}}}return result;}

局部变量 result 的使用,能够提升 25% 的性能。

如果可以接受实例域的重复初始化,可以使用单重检查模式 single check idiom:

private volatile FieldType field;FieldType getInstance() {FieldType result = field;if (result == null) {field = result = computeFieldValue();}return result;}

去掉 volatile 可以加快某些架构上的访问,代价是增加了额外的初始化,这种模式叫 racy single check idiom,在 String 的散列码字段 hash 的初始化中用到。

private int hash; // Default to 0public int hashCode() {int h = hash;if (h == 0 && value.length > 0) {char val[] = value;for (int i = 0; i 31 * h + val[i];}hash = h;}return h;}

不要依赖于线程调度器

任何依赖于线程调度器来达到正确运行或者性能要求的程序,很可能都是不可移植的。要编写健壮的、响应良好的、可移植的应用程序,最好的办法是设置合理的线程数目,减少无意义的工作(比如 busy-wait)。

不要依赖 Thread.yield 或线程优先级,它们不具有可移植性,也就是说在不同 JVM 上表现不同。

避免使用线程组 ThreadGroup

线程组的实现由很多问题,使用线程池 executor 即可。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3